﻿using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Client.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
using System.Security.Claims;
using OpenIddict.Server.AspNetCore;

namespace Magic.Oidc.MvcClient.Controllers
{
    public class AuthenticationController : Controller
    {
        [HttpGet("~/login")]
        public ActionResult LogIn(string returnUrl)
        {
            var properties = new AuthenticationProperties(new Dictionary<string, string>
            {
                // Note: when only one client is registered in the client options,
                // setting the issuer property is not required and can be omitted.
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:7000/"
            })
            {
                // Only allow local return URLs to prevent open redirect attacks.
                RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
            };

            // Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
            return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        [HttpPost("~/logout"), ValidateAntiForgeryToken]
        public async Task<ActionResult> LogOut(string returnUrl)
        {
            // Retrieve the identity stored in the local authentication cookie. If it's not available,
            // this indicate that the user is already logged out locally (or has not logged in yet).
            var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            if (result is not { Succeeded: true })
            {
                // Only allow local return URLs to prevent open redirect attacks.
                return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");
            }

            // Remove the local authentication cookie before triggering a redirection to the remote server.
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            var properties = new AuthenticationProperties(new Dictionary<string, string>
            {
                // Note: when only one client is registered in the client options,
                // setting the issuer property is not required and can be omitted.
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:7000/",

                // While not required, the specification encourages sending an id_token_hint
                // parameter containing an identity token returned by the server for this user.
                [OpenIddictClientAspNetCoreConstants.Properties.IdentityTokenHint] =
                    result.Properties.GetTokenValue(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken)
            })
            {
                // Only allow local return URLs to prevent open redirect attacks.
                RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
            };

            // Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
            return SignOut(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        // Note: this controller uses the same callback action for all providers
        // but for users who prefer using a different action per provider,
        // the following action can be split into separate actions.
        [HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
        public async Task<ActionResult> LogInCallback()
        {
            // Retrieve the authorization data validated by OpenIddict as part of the callback handling.
            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);

            // Multiple strategies exist to handle OAuth 2.0/OpenID Connect callbacks, each with their pros and cons:
            //
            //   * Directly using the tokens to perform the necessary action(s) on behalf of the user, which is suitable
            //     for applications that don't need a long-term access to the user's resources or don't want to store
            //     access/refresh tokens in a database or in an authentication cookie (which has security implications).
            //     It is also suitable for applications that don't need to authenticate users but only need to perform
            //     action(s) on their behalf by making API calls using the access token returned by the remote server.
            //
            //   * Storing the external claims/tokens in a database (and optionally keeping the essential claims in an
            //     authentication cookie so that cookie size limits are not hit). For the applications that use ASP.NET
            //     Core Identity, the UserManager.SetAuthenticationTokenAsync() API can be used to store external tokens.
            //
            //     Note: in this case, it's recommended to use column encryption to protect the tokens in the database.
            //
            //   * Storing the external claims/tokens in an authentication cookie, which doesn't require having
            //     a user database but may be affected by the cookie size limits enforced by most browser vendors
            //     (e.g Safari for macOS and Safari for iOS/iPadOS enforce a per-domain 4KB limit for all cookies).
            //
            //     Note: this is the approach used here, but the external claims are first filtered to only persist
            //     a few claims like the user identifier. The same approach is used to store the access/refresh tokens.

            // Important: if the remote server doesn't support OpenID Connect and doesn't expose a userinfo endpoint,
            // result.Principal.Identity will represent an unauthenticated identity and won't contain any claim.
            //
            // Such identities cannot be used as-is to build an authentication cookie in ASP.NET Core (as the
            // antiforgery stack requires at least a name claim to bind CSRF cookies to the user's identity) but
            // the access/refresh tokens can be retrieved using result.Properties.GetTokens() to make API calls.

            //  有多种策略可以处理OAuth 2.0/OpenID Connect回调，每种策略都有其优缺点：

            // * 直接使用令牌代表用户执行必要的操作(S)，
            // 这适用于不需要长期访问用户资源或不想将访问/刷新令牌
            // 存储在数据库或身份验证Cookie中(这具有安全含义)的应用程序。
            // 它也适用于不需要对用户进行身份验证，
            // 而只需要通过远程服务器返回的访问令牌进行接口调用来代表用户执行操作(S)的应用。
            // 

            // *将外部声明/令牌存储在数据库中
            // (并可选择将基本声明保留在身份验证Cookie中，以便不会达到Cookie大小限制)。
            // 对于使用ASP.NET Core Identity的应用程序，
            // 可以使用UserManager.SetAuthenticationTokenAsync() API存储外部令牌。

            // 注意：在上面👆这种情况下，建议使用列加密来保护数据库中的令牌。

            // *将外部声明/令牌存储在身份验证Cookie中，
            // 身份验证Cookie不需要用户数据库，
            // 但可能受大多数浏览器供应商强制实施的Cookie大小限制
            // (例如，针对MacOS的Safari和针对iOS/iPadOS的Safari对所有Cookie强制每个域4KB的限制)。

            // 注意：这是这里使用的方法，但首先对外部声明进行过滤，以仅保存一些声明，如用户标识符。
            // 使用相同的方法来存储访问/刷新令牌。

            // 重要提示：如果远程服务器不支持OpenID Connect并且不公开用户信息终结点，
            // Identity将表示未经身份验证的身份，并且不包含任何声明。

            // 此类标识不能按原样用于在ASP.NET Core中构建身份验证Cookie(作为
            // 防伪堆栈需要至少一个名称声明才能将CSRF Cookie绑定到用户的身份)，但是。
            // 可以使用Result.Properties.GetTokens()进行API调用来检索访问/刷新令牌。
            if (result.Principal.Identity is not ClaimsIdentity { IsAuthenticated: true })
            {
                throw new InvalidOperationException("The external authorization data cannot be used for authentication.");
            }

            // Build an identity based on the external claims and that will be used to create the authentication cookie.
            //
            // By default, all claims extracted during the authorization dance are available. The claims collection stored
            // in the cookie can be filtered out or mapped to different names depending the claim name or its issuer.
            // 基于外部声明构建身份，并将用于创建身份验证Cookie。
            //
            // 默认情况下，在授权舞蹈过程中提取的所有声明都可用。
            // 存储在Cookie中的索赔集合可以根据索赔名称或其颁发者被筛选出来或映射到不同的名称。
            var claims = new List<Claim>(result.Principal.Claims
                .Select(claim => claim switch
                {
                    // Map the standard "sub" and custom "id" claims to ClaimTypes.NameIdentifier, which is
                    // the default claim type used by .NET and is required by the antiforgery components.
                    // 将标准的“Sub”和自定义的“id”声明映射到ClaimTypes.NameIdentifier，
                    // 这是.NET使用的默认声明类型，也是防伪组件所必需的。
                    { Type: Claims.Subject }
                        => new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),

                    // Map the standard "name" claim to ClaimTypes.Name.
                    // 将标准的name声明映射到ClaimTypes.Name。
                    { Type: Claims.Name }
                        => new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),

                    _ => claim
                })
                .Where(claim => claim switch
                {
                    // Preserve the basic claims that are necessary for the application to work correctly.
                    // 保留应用程序正常工作所必需的基本声明。
                    { Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,

                    // Don't preserve the other claims.
                    // 不要保留其他声明。
                    _ => false
                }));

            var identity = new ClaimsIdentity(claims,
                authenticationType: CookieAuthenticationDefaults.AuthenticationScheme,
                nameType: ClaimTypes.Name,
                roleType: ClaimTypes.Role);

            // Build the authentication properties based on the properties that were added when the challenge was triggered.
            // 根据质询触发时添加的属性构建身份验证属性
            var properties = new AuthenticationProperties(result.Properties.Items);

            // If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
            // To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
            // 如果需要，授权服务器返回的令牌可以存储在鉴权cookie中。
            // 为了减轻Cookie的重量，在创建Cookie之前会过滤掉不使用的Token。
            properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
            {
                // Preserve the access, identity and refresh tokens returned in the token response, if available.
                // 保留令牌响应中返回的访问、身份和刷新令牌(如果可用)。
                {
                    Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
                } => true,

                // Ignore the other tokens.
                // 忽略其他令牌。
                _ => false
            }));

            // Ask the cookie authentication handler to return a new cookie and redirect
            // the user agent to the return URL stored in the authentication properties.
            // 请求Cookie身份验证处理程序返回新的Cookie，
            // 并将用户代理重定向到身份验证属性中存储的返回URL。
            //return SignIn(new ClaimsPrincipal(identity), properties, CookieAuthenticationDefaults.AuthenticationScheme);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), properties);
            return Redirect(properties.RedirectUri);
        }
        // Note: this controller uses the same callback action for all providers
        // but for users who prefer using a different action per provider,
        // the following action can be split into separate actions.
        // 注意：此控制器对所有提供程序使用相同的回调操作，
        // 但对于喜欢对每个提供程序使用不同操作的用户，
        // 可以将以下操作拆分为单独的操作。
        [HttpGet("~/callback/logout/{provider}"), HttpPost("~/callback/logout/{provider}"), IgnoreAntiforgeryToken]
        public async Task<ActionResult> LogOutCallback()
        {
            // Retrieve the data stored by OpenIddict in the state token created when the logout was triggered.
            // 注意：此控制器对所有提供程序使用相同的回调操作，但对于喜欢对每个提供程序使用不同操作的用户，可以将以下操作拆分为单独的操作。

            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);

            // In this sample, the local authentication cookie is always removed before the user agent is redirected
            // to the authorization server. Applications that prefer delaying the removal of the local cookie can
            // remove the corresponding code from the logout action and remove the authentication cookie in this action.
            // 在此示例中，总是在用户代理重定向到授权服务器之前删除本地身份验证Cookie。
            // 喜欢延迟删除本地Cookie的应用程序可以从注销操作中删除相应的代码，
            // 并在此操作中删除身份验证Cookie。
            return Redirect(result!.Properties!.RedirectUri);
        }
    }
}
